package randomWalkWithRestartRecommender;

import Jama.Matrix;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator;
import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;
import org.apache.mahout.cf.taste.model.PreferenceArray;

import java.util.ArrayList;
import java.util.List;


public final class GroupLensRecommender {

    private static final int MAX_ITERATIONS = 10;
    private static final double EPS = 0.01;
    private static final double RESTART_PROBABILITY = 0.85;

    private final org.apache.mahout.cf.taste.model.DataModel dataModel;
    private int maxUserNum;
    private int maxItemNum;
    private int maxNum;

    public GroupLensRecommender(GroupLensDataModel ratingsModel) throws TasteException {
        dataModel = ratingsModel;
        maxItemNum = ratingsModel.maxItemNum();
        maxUserNum = ratingsModel.maxUserNum();
        maxNum = maxItemNum + maxUserNum;
    }

    public List<Integer> recommend(int userID, int howMany) throws TasteException {
        if (userID > maxNum) {
            throw new RuntimeException("There is no user with such id: " + userID);
        }
        Matrix resVector = randomWalk(userID, EPS, MAX_ITERATIONS, RESTART_PROBABILITY);
        List<Integer> result = new ArrayList<Integer>();
        for (int i = 0; i < howMany; i++) {
            int maxArgIndex = 0;
            double maxArgID = resVector.get(0, 0);
            for (int k = 0; k < resVector.getRowDimension(); k++) {
                if (resVector.get(k, 0) > maxArgID) {
                    maxArgID = resVector.get(k, 0);
                    maxArgIndex = k;
                }
            }
            resVector.set(maxArgIndex, 0, 0.0);
            result.add(maxArgIndex);
        }
        return result;
    }

    private Matrix buildEuclidianSubmatrix(boolean isForUser) throws TasteException {
        int num;
        int ids[];
        if (isForUser) {
            System.out.println("Building userUserSubmatrix...");
            num = maxUserNum;
            ids = getUserIDs();
        } else {
            System.out.println("Building itemItemSubmatrix...");
            num = maxItemNum;
            ids = getItemIDs();
        }

        Matrix result = new Matrix(num, num);
        GroupLensDataModel model = (GroupLensDataModel) dataModel;
        EuclideanDistanceSimilarity similarity = new EuclideanDistanceSimilarity(model);

        for (int i = 0; i < ids.length; i++) {
            for (int j = 0; j < ids.length; j++) {
                if (ids[i] != 0 && ids[j] != 0) {
                    Double val;
                    if (isForUser) {
                        val = similarity.userSimilarity(ids[i], ids[j]);
                    } else {
                        val = similarity.itemSimilarity(ids[i], ids[j]);

                    }
                    if (val.isNaN()) {
                        val = 0.0;
                    }
                    result.set(ids[i] - 1, ids[j] - 1, val);
                }
            }
        }
        return result;
    }

    private Matrix buildItemUserSubmatrix() throws TasteException {
        Matrix itemUser = new Matrix(maxItemNum, maxUserNum);

        System.out.println("Building itemUserSubmatrix...");
        GroupLensDataModel model = (GroupLensDataModel) dataModel;
        for (LongPrimitiveIterator it = model.getUserIDs(); it.hasNext();) {
            Long currentID = it.nextLong();
            PreferenceArray currentPreference = model.getPreferencesFromUser(currentID);
            for (int j = 0; j < currentPreference.length(); j++) {
                Long itemID = currentPreference.getItemID(j);
                double value = currentPreference.getValue(j);
                itemUser.set(itemID.intValue() - 1, currentID.intValue() - 1, value / 10);
            }
        }
        return itemUser;
    }


    public int[] getItemIDs() throws TasteException {
        GroupLensDataModel model = (GroupLensDataModel) dataModel;
        int ids[] = new int[maxItemNum];
        int i = 0;
        for (LongPrimitiveIterator it = model.getItemIDs(); it.hasNext();) {
            Long currentID = it.nextLong();
            ids[i] = currentID.intValue();
            i++;
        }
        return ids;
    }

    public int[] getUserIDs() throws TasteException {
        GroupLensDataModel model = (GroupLensDataModel) dataModel;
        int ids[] = new int[maxUserNum];
        int i = 0;
        for (LongPrimitiveIterator it = model.getUserIDs(); it.hasNext();) {
            Long currentID = it.nextLong();
            ids[i] = currentID.intValue();
            i++;
        }
        return ids;
    }


    private Matrix randomWalk(int startingNode, double delta, int maxIterations, double restartProbability) throws TasteException {
        Matrix userUser = buildEuclidianSubmatrix(true);
        Matrix itemItem = buildEuclidianSubmatrix(false);
        Matrix itemUser = buildItemUserSubmatrix();
        Matrix startingVector = new Matrix(maxNum, 1);

        startingVector.set(startingNode, 0, 1);
        Matrix x = new Matrix(maxNum, 1);
        int iteration = 0;
        while (!hasConverged(startingVector, x, delta)) {
            //1st component
            Matrix s1 = startingVector.times(restartProbability);
            //2nd component
            Matrix s2 = new Matrix(maxNum, 1);
            x = startingVector;
            //vectors - rows
            Matrix a, b, c, d;
            c = new Matrix(1, maxNum);

            for (int i = 0; i < maxNum; i++) {
                if (i < maxItemNum) {
                    a = itemItem.getMatrix(i, i, 0, maxItemNum - 1);
                    b = itemUser.getMatrix(i, i, 0, maxUserNum - 1);
                } else {
                    //column
                    d = itemUser.getMatrix(0, maxItemNum - 1, i - maxItemNum, i - maxItemNum);
                    a = d.transpose();
                    b = userUser.getMatrix(i - maxItemNum, i - maxItemNum, 0, maxUserNum - 1);
                }
                concat(a, b, c);
                normalizeRow(c);
                c.timesEquals(1 - restartProbability);
                s2 = c.times(x);
                startingVector.set(i, 0, s2.get(0, 0));
            }
            startingVector = s1.plus(startingVector);
            iteration++;
            if (iteration > maxIterations) {
                break;
            }
        }
        return startingVector;
    }

    private boolean hasConverged(Matrix a, Matrix b, double delta) {
        double sum1 = 0.0;
        double sum2 = 0.0;
        for (int i = 0; i < a.getRowDimension(); i++) {
            sum1 += (a.get(i, 0) - b.get(i, 0)) * (a.get(i, 0) - b.get(i, 0));
            sum2 += a.get(i, 0) * a.get(i, 0);
        }
        if (sum1 / sum2 > delta) {
            return false;
        } else {
            return true;
        }
    }

    private void concat(Matrix a, Matrix b, Matrix c) {
        for (int i = 0; i < maxNum; i++) {
            if (i < maxItemNum) {
                c.set(0, i, a.get(0, i));
            } else {
                c.set(0, i, b.get(0, i - maxItemNum));
            }
        }
    }

    private void normalizeRow(Matrix c) {
        double norm = c.normInf();
        if (norm != 0) {
            c = c.timesEquals(1 / norm);
        }
    }

}
